العمليات و الإشارات


برنامج البيرل يمكن أن يشغل برنامج اخر أو حتي نسخ نسخة شبية من نفسة وذلك لتقسم العمل بين العمليات ،وتسمح الأنابيب لسكربت البيرل من تبادل البيانات مع العمليات الاخرى ، ومن الممكن مراقبة وسيطرة أسكربت البيرل للعمليات الاخرى بواسطة الإشارات .


العمليات (Processes)

في أنظمة اليونكس والونيدوز و VMS والأنظمة الاخرى الحديثة متعددة المهام multitasking وتعدد المهام هي قدرة النظام على تنفيذ أكثر من عملية على الجهاز في نفس الوقت ، فمثلاً عند طباعة ملف يمكن في نفس الوقت كتابة ملف اخر على الجهاز وفي نفس الوقت تنفيذ عملية بحث عن ملف معين ،وايضاً يمكن لنظام التشغيل أن يشغل عدة برامج في نفس الوقت مايعرف بتعدد البرامج multiprogram ، وكل برنامج يشغل في thread منفصله من التنفيذ وتعرف بالـعملية process .

العمل job مجموعة من العمليات المختلفة اللازمة لتنفيذ ما يطلبة المستخدم حيث يتم جدولة العمل الى العديد من العمليات في الذاكرة ثم معالجتها وتنفيذها في المعالج processor المناسب ، ومفهوم العملية process هي جزء من العمل أثناء التنفيذ ، فعلى سبيل المثال برمجه السيرفر في الشبكات يحتاج إلى معالجة ما هو المطلوب من العديد من العملاء في اغلب الاوقات أو قد يطلب المقبض الطلب نفسه في نفس الوقت ، ببساطة تعدد المهام في برنامجك شيء رائعة لانها تسمحلك لإمكانية عمل عملية جديدة منفصله مع كل الاشياء التي البرنامج يحتاج إنجازها، وكل عملية من تلك العمليات منفصله عن الاخرى ، كما ان العملية الواحدة تسمح للحصول على العمل بدون القلق عن الاخرين عند العودة اليها .


تدعم البيرل نوعين من تعدد المهام :


  1. النوع الأول مستند على اليونكس التقليدي المتعدد العمليات multiprocessing ، يسمح للعملية الحالية للإستنساخ نفسها بعمل استدعاء للدالة fork ،وبعد تنفيذ الدالة تتواجد عمليتين مشغلة في الذاكرة ، كلاً منهما مماثلة الى الاخر تقريباًُ تذهب الاوله لعمل مهمة بينما تذهب الاخرى الى عمل مهمة اخرى .

  2. النوع الاخر يستند على "thread"وتحفظ كل المهام ضمن عملية وحيده ، ومن الممكن ان يأخذ البرنامج العديد من threads لتشغيل التنفيذ من خلاله ، وكل منها تشغل بشكل منفصل عن البقية.


الدالة fork()

فهذة الدالة لن تأخذ اي وسيط وتعيد ناتج عددي من الكود ، وعند إستدعاء الدالة ستقوم بتوليد نسخة مطابقة من العملية الحالية تسمى النسخة المستنسخه بالـ child وتشترك في القيم الحالية من كل المتغيرات ومقابض الملف (البيانات المتضمنة في standard I/O buffers ) والهياكل البيانات الاخرى ، وستكون أيضاً للعملية المستنسخه ذاكرة من إستداعى الدالة fork .

صيغة الدالة fork

pid = fork();    # if the fork succeeds,pid > 0 in the parent
if(pid < 0 ) {
 die "fork failed $!";  # e.g.,memory or some table is full
} else if(pid > 0) {
...parent code goes here
}
else {
...child code goes here 
}

كل عملية في النظام لديها عدد صحيح موجب فريد مقترن يدعى بمعرف العملية process ID اختصاره PID :

#!usr/bin/perl 
#file :fork.pl
print "PID=$$\n";

my $child=fork();
die "can't fork:$!"unless defined $child;
if($child>0) { # parent process 
print "parent process:PID=$$,child=$child\n";

}
else { # child process 
my $ppid=getppid();
print "child process: PID=$$,parent=$ppid\n";
}

عن تشغيل fork.pl:


Output

% fork.pl
PID=372
Parent process: PID=372, child=373
Child process:  PID=373, parent=372



$$

المتغير الخاص $$ فهو يحمل الـ PID للعملية الحالية ، نستطيع عرضة ولكن لايمكن تغيره .

$child=fork()

هنا يحدث التفرع لإنشاء عملية جديدة وبعد استدعاء دالة fork يفحص الأب و الطفل القيمة العادة للداله بواسطة الشرط ، فيتم إختبار عملية الأب parent process عن طريق ما تعيدة الدالة ففي عملية الطفل child process تعيد العدد صفر ، وتعيد غير معرف undef في حالة الخطاء failed (مثلاً الذاكرة غير كافية للتفرع ) ويوضع المتغير الخاص $! للرسائلة الخطاء الملائمة وينتهي die البرنامج برسائلة خطاء .

ونجد أن برنامج الأب يحصل على PID الطفل من ناتج الدالة fork المخصص الى المتغير $child ، اما الـطفل يحصل على PID الأب بواسطة استدعى getppid() ، ولمعرفة PID الحالية للعملية عن طريق فحص المتغير الخاص $$.


الصيغة:

$ppid=getppid() 

تعيد الدالة الـPID للعملية الأب parent process ، ولاحظ ان الأب أطول عمراً من الطفل أي ينتهي بعد نهاية أطفاله ، فجميع اسكربتات البيرل لديها أب حتى الذين يشغلوا مباشراً من سطر الاوامر لديهم أب (وهي عملية الشل )، ويمكن أيضاً عمل تفرع للـعملية الطفل child process بنفسها وإنشاء حفيد ، ويمكن تفرع الأب الاصل مرة اخرى حتى اننا نسطيع عمل تفرع للإطفال والأحفاد في نفس الطريقة السابقة تماماً وإنشاء قبيلة من العمليات ،وكل عضو في هذه القبيلة يسكن الى نفس المجموعة من العمليات تسمى بمجموعة العملية process group ، ولدى مجموعة العملية ايضاً معرف ID وحيد وفريد الذي يماثل معرفة العملية سابقاً process ID وقيمتها يمكن ان نحصل عليه بواسطة الدالة getpgrp():

$processid = getpgrp([$pid])

تعيد هذة الدالة معرف مجموعة العملية process group ID للعملية المحددة في المتغير $pid ، وأن لم نحدد PID فسوف تعيد الدالة مجموعة العملية للـ PID الحالي ، وبشكل خاص كل عضو مشترك في مجموعة العملية فسوف يشتركون في نفس STDINو STDOUTو STDERR حينما يفتح المقبض في نفس الوقت الاب يفرع ، وبامكان اي طفل ان يعدل هذا المقبض فمثلاً بغلق المقبض أو بإعادة فتحه على مصدر اخر ، على اي حال يتتبع النظام الأطفال الذين لديهم المقابض مفتوحة ولن يغلق الملف أو الاتصال حتى يغلق اخر طفل نسخته من المقبض .


الدالة system()

الدالة system طريقة اخرى لتشغيل عملية فرعية subprocess ، فهي تشغل البرنامج الاخر كعملية فرعية في العملية الحالية ، وتنتظر الى ان يكتمل تنفيذ البرنامج بعد ذلك تعيد الناتج الى العملية الحالية ، أذا نجحنا في التشغيل تعيد الدالة القيمة صفر ( ولاحظ تختلف عن صياغة البيرل) وغير ذلك تعيد -1 لعدم وجود البرنامج أو ان البرنامج في حالة خروج exit status للوجود اي خطأ في البرنامج المشغل ولمعرفة التفاصيل التي تاتي مع $? انظر الى perldoc perlvar

$status = system ('command and arguments') 
$status = system ('command', 'and', 'arguments')

يلاحظ من خلال الصيغة تحديد الامر والوسيط كأنص واحد أو مع العديد من النصوص فالدالة النظام system تنفذ الامر كعملية فرعية وتنظر الى ان يكتمل تنفيذة على الشل ، ومن الممكن عمل للامر (input/output ,re-directs ) للمرارها الى الشل .


الدالة exec()

الدالة exec هي نفس الدالة system لكنها تستبدل العملية الحالية بالامر المشار ، فاذا نجح التشغيل فهي لاتعيد ابداً لان العملية نفسها هي من تعبر ، والعملية الجديدة سوف تأخذ نفس PID الأقدم وسوف تشترك بنفس الـ STDIN, STDOUT, STDERR للمقابض الملف ، فمن خلال الدالة exec فالمقابض المفتوحة تغلق بشكل آلي ، لكن بامكان ان ترتيب بعض المقابض ليبقون مفتوحين عبر الدالة exec بواسطة التغير في المتغير الخاص $~F شاهد الصفحة perlvar

$status = exec ('command and arguments') 
$status = exec ('command', 'and', 'arguments') 

تنفذ الامر وتستبدل بالعملية الحالية ، فاذا نجحت الدالة لن تعيد اي شيء ، سوف تعيد فقط في حالة الفشل وغير ذلك لاتعيد اي شيء .

في حالة عمل برنامج باستعمال الدالة fork سيعمل شيء اذا اكتشف انة الأب وعمل شيء اخر اذا كان الطفل

my $child = fork();
die "Can't fork: $!" unless defined $child;
if ($child == 0) { # we are in the child now
  open (STDOUT,">log.txt") or die "open() error: $!";
  exec ('ls','-l');
  die "exec error(): $!"; # shouldn't get here
}

لاحظ اننا الان في عملية الطفل ، فادالة exec مع الدالة fork تقوم بتنفيذ الامر كعملية فرعية بدلاً من استبدال العملية الحالية وذلك بعد اعادة فتح الـ STDOUT على الملف وثم استدعاء الدالة exec لتشغيل الامر ls -l ،فهذا التأثير سوف يشغل الامر في الخلفية ثم يكتب الخرج الى الملف المشار .

الامر ps لمشاهد مداخل جدولة العلميات

@a=exec "ps -al";
print @a,"\n";


الأنابيب Pipes

برمجة الشبكات تعتمد كلها حول interprocess communication (IPC) ( هو الشئ الذي يحدث عندما برنامج يتحدث مع اخر) فالعملية تبدل البيانات مع عملية اخرى بالإعتماد على التطبيق المصمم ، فقد تشغلان العمليتين على نفس الجهاز وقد تشغل على جهازين في شبكة محلية أو قد يكون جهازين في الشبكة بين جهازين متصلين عن بعد ، أو قد تكون العمليتين متعلقة ببعضهما البعض مثلاً عملية تشغل للسيطرة على العملية الاخرى .

ان الشيء الاسهل للـ IPC هي الأنبوب pipe ، فانبوب هو مقبض للملف بالسكربت الحالي الذي يتصل بالدخل القياسي أو بالخرج القياسي للعملية الاخرى .

ولكي نفتح أنبوب نستعمل الدالة open بواسطة البارامتر الثاني بالرمز " | " فأما ان تسبق الوسيط الثاني أو ياتي بعد الوسيط ، ونضع الامر (أو برنامج بيرل ) كما يتم وضعة في الشل على اليونكس والدوز على الونيدوز ويمكن ان تدخل مسار البرنامج كاملاًُ مثلاً /usr/bin/ls أو أدخل الامر فقط اذا كان معرف PATH في متغير environment .


البيرل شبية في أستخدام الانابيب في اليونكس لأتصال برنامجين معاً ، فمثلاًَ ستحتاج الى الامر التالي عن طريق بعض الخطوات في حالة لم نستخدم الانبوب

$ ps > psout.txt
$ sort psout.txt > pssort.out

لإتصال برنامجين بالأنبوب

$ ps | sort > pssort.out

يعيد الانبوب خرج برنامج ps إلى مدخل برنامج sort



عادتاً في شل اليونكس يتم الاتصال بين الخرج القياسي للبرنامج الى الدخل القياسي للبرنامج أخر،ويمكن للبيرل القراءة من الانبوب أوالكتابة الية، في حال سبق رمز الأنبوب اسم البرنامج فأن مقبض الملف مفتوح للكتابة وكل شيء تكتبة الى مقبض الملف فسوف يرسل الى standard input البرنامج

open(FH, "| perl pro.pl");

في حال اتى رمز الأنبواب بعد البرنامج فأن مقبض الملف مفتوح للقراءة وكل شيء يقراء من مقبض الملف هو مأخوذ من standard output البرنامج .

open(FH, "perl pro.pl |");

Piping In

فعلى سبيل المثال الامر ls -l يقوم بأعادة قائمة الملفات التي في المجلد الحالي ، وعند أخذ البارامتر " ls -l | " للفتح يمكننا ان نفتح الأنبواب للقراءة من الامر :

open (LSFH,"ls -l |") or die "Can't open ls -l: $!";
while (my $line = <LSFH>) {
  print "I saw: $line\n";
}
close LSFH;

Piping Out

والامر wc يقوم بحساب جميع عدد الاسطر والكلمات والحروف ، الخيار "-l " يحسب عدد الاسطر ، والخيار "-w " يحسب عدد الكلمات

open (WC,"| wc -lw") or die "Can't open wordcount: $!";
print WC "This is the first line.\n";
print WC "This is the another line.\n";
print WC "This is the last line.\n";
print WC "Oops. I lied.\n";
close WC;

سيقوم بكتابة بعض الاسطر من النصوص اليه ، ثم يغلق الأنبوب ، فعند تشغل البرنامج تخرج عدد الكلمات والسطور بواسطة الامر وتطبع في سطر الاوامر .

وبالنظر الى المثال اسفل فسوف ينفتح الأنبوب للامر who بتقدير عدد مرات دخول كل مستخدم وينتج تقرير في الناتج

#!/usr/bin/perl
# file: whos_there.pl
use strict; 
my %who;   # accumulate logins 

open (WHOFH,"who |") or die "Can't open who: $!"; 

while (<WHOFH>) { 
    next unless /^(\S+)/; 
    $who{$1}++; 
} 

foreach (sort {$who{$b}<=>$who{$a}} keys %who) { 
    printf "%10s %d\n",$_,$who{$_}; 
}

close WHOFH or die "Close error: $!";

Output

% whos_there.pl
  jsmith 9
     abu 5
  lstein 1
 palumbo 1



نشاهد من الخرج بأن المستخدمين jsmith و abu و lstein و palumbo وعدد مرات الدخول المقابل لكل مستخدم و المستخدمين مرتبين تنازلياً بعدد مرات الدخول ، فيبداء البرنامج بتعريف الهاش %who ثم فتح الانبوب للامر who بواسطة الدالة open ثم قراءة خرج الامر who بحيث يقراء من عملية خرج who سطر سطر ويبدو السطر من الخرج كهذا مثلاً

jsmith pts/23 Aug 12 10:26 (cranshaw.cshl.org)

وبإستخدام نمط المطابقة لاستخراج اسم المستخدم وتخزين الاسماء في الهاش %who التي سوف تصبح مفاتيح الهاش ووضع القيم بعدد المرات الدخول لكل مستخدم ،ويتوقف التكرار حتي نهاية البيانات في المقبض WHOFH أو عندما البرنامج الاخر يكون في نهاية الأنبوب أو بإغلاق الخرج القياسي للانبوب .

ثم طباعة الناتج من خلال مفاتيح الهاش المرتبة للقيم لكل مستخدم له عدد مرات الدخول تنازلياً ، ثم طباعة بواسطة الدالة printf لإنجاز صيغة النصوص %10s وابتعاد بعشر مسافات الى اليمين وطباعة قيمة الهاش بانجاز %d للأعدد الصحيحة



يمكن بطريقة اسهل لتعامل مع الاوامر وتخزين خرج الامر بواسطة مصفوفة أو هاش بإستخدام معامل Backtick ( ` )

$arguments = '-l -F';
$ls_output = `ls $arguments`;

يقوم المعامل بتنفيذ الامر في نافذة الاوامر ثم تخزين ناتج الامر الى متغير ls_output

لكن الخطأ القياسي للعملية الفرعية لان يوجة عن طريق رمز backticks ، فاذا كتبت العملية فرعية أي تشخيص أو رسائلة خطأ فسوف يمزج مع برنامجنا ، وعلى نظام اليونكس نستطيع على شل Bourne من توجية الخرج لدمج الخطأ القياسي للعملية الفرعية مع الخرج القياسي لها كهذا :

$ls_output = `ls 2>&1`;

يصبح الان خرج $ls_output يحتوي على كلاً من الخطأ القياسي والخرج القياسي للامر .



الدالة pipe()

ان الشيء الافضل إستخدام الدالة pipe لكنها معقدة بعض الشيء لإنشاء أنبواب حيث تقوم بإنشاء زوج من مقابض الملفات واحد للقراءة والاخر للكتابة فكل شيء يكتب الى المقبض الاول يمكن ان يقراء من الاخر .

الصيغة :

$result = pipe (READHANDLE,WRITEHANDLE)

فتح زوج من المقابض متصلة بالأنبوب ، فأول وسيط اسم المقبض للقراءة منة وثاني وسيط هو المقبض للكتابة الية فاذا نجح الانبواب فسوف تعيد الدالة قيمة صحيحة الى $result


لماذا الدالة pipe مفيدة ؟
تستخدم الدالة عموماً في الاتصالات مع الدالة fork لإنشاء الاب والطفل التي تستطيع تبادل البيانات فيما بينها ، فعملية الأب parent process تحتفظ بمقبض واحد وتغلق الأخر بينما الطفل يعمل العكس ، حيث ان عملية الاب والطفل يتصلان عبر الأنبوب بالتوازي ، ولتوضيح ذلك هنا مثال مرفق يصور قوة هذة التقنية بإستخدام الدالة fork فالمعطى عدد صحيح موجب والسكربت facfib.pl يحسب المضروب factorial وقيمة موقعة في سلسلة Fibonacci ، ان الفوائد الحديثة لاليآت تعدد العمليات multiprocessing هي لإنجاز الحسابات من خلال عمليتين فرعيتين وتنجز الحسابات في النظام المتوازي ، والسكربت يستخدم الدالة pipe لإنشاء مقابض الملفات التي في عمليات الطفل التي تستعمل للإبلاغ أو لإيجاد نتائجهم الى عملية الأب التي تشغلهم ، وعند تشغيل هذا البرنامج نشاهد نتائج مثل هذا :


Output

% facfib.pl 8
factorial(1) => 1
factorial(2) => 2
factorial(3) => 6
factorial(4) => 24
factorial(5) => 120
fibonacci(1) => 1
factorial(6) => 720
fibonacci(2) => 1
factorial(7) => 5040
fibonacci(3) => 2
factorial(8) => 40320
fibonacci(4) => 3
fibonacci(5) => 5
fibonacci(6) => 8
fibonacci(7) => 13
fibonacci(8) => 21



فهناك تداخل في نتائج الـfactorial و الـFibonacci بسبب انهم يحدثون بالتوازي ولمشاهدة آلية عمل البرنامج :


Using pipe() to create linked filehandles :

#!/usr/bin/perl
# file: facfib.pl

use strict;
my $arg = shift || 10;

pipe(READER,WRITER) or die "Can't open pipe: $!\n";

if (fork == 0) { # first child writes to WRITER
  close READER;
  select WRITER; $| = 1;
  factorial($arg);
  exit 0;
}

if (fork == 0) { # second child writes to WRITER
  close READER;
  select WRITER; $| = 1;
  my $result = fibonacci($arg);
  exit 0;
}

# parent process closes WRITER and reads from READER
close WRITER;
print while <READER>;

sub factorial {
  my $target = shift;
  for (my $result = 1,my $i = 1; $i <= $target; $i++) {
    print "factorial($i) => ",$result *= $i,"\n";
  }
}

sub fibonacci {
  my $target = shift;
  for (my $result = 1,my $i = 1; $i <= $target; $i++) {
    print "fibonacci($i) => ",$result += $i,"\n";
  }
}

لإنشاء أنابيب مترابطة linked pipes نستخدم الدالة pipe حيث المقبض READER سوف يستخدم في عملية البرنامج الرئيسي (عملية الأب) للقراءة النتائج من الاطفال ، اما الاطفال سوف نستخدم لها المقبض WRITER لكتابة نتائجهم.

وفي بلوك عملية الطفل الاول نستدعي الدالة fork للإستنساخ العملية الحالية ، حيث لانستطيع عمل ذلك في عملية الأب فالدالة fork لن تعيد قيمة PID بالصفر لكن في عملية الطفل تعيد القيمة العددية صفر ونستطيع استدعاء الدالة في هذة الصيغة ، ثم بإغلاق مقبض الملف READER وذلك بسبب اننا لان نحتاجة ويمكن تحديد المقبض WRITER بواسطة الدالة select لجعلة الأفتراضي للخرج مع فتح النمط autoflush بوضع المتغير الخاص $| بالقيمة true وذلك لضمان حصول عملية الأب على الرسائل حالما نكتبها ، ثم استدعاء الدالة factorial مع اول وسيط مرسل من نأفذة الاوامر وبعد ذلك فأن عملية الطفل تكون قد اكتملت لذا للخروج منها بواسطة exit (للرجوع الي البرنامج الرئيسي عملية الأب ) والمقبض المنسوخ WRITER يغلق بشكل آلياً .

وبنفس العمل سوف نضيف عملية طفل ثانية في عملية الأب لكن في هذة العملية الفرعية استدعى الدالة fibonacci بدلاً من factorial

ثم لقراءة رسائل عمليات الاطفال في عملية الأب ، بإغلق المقبض WRITER الذي لان نحتاجة أيضاً ثم القراءة من المقبض READER سطر سطر في كل مرة وطباعة الناتج حيث يحتوي على كل السطور في الخرج القياسي لجميع الأطفال ، فالمقبض READER سوف يعيد undef عندما اخر طفل ينتهي من التنفيذ ويغلق مقبض WRITER الخاص بها وترسل لنا بالـ EOF ، ونستطيع اغلاق المقبض READER بالدالة close اثناء القراءة وفحص الكود الناتج أو نستطيع اخبار مفسر البيرل بإغلاق المقبض عند الخروج من التكرار.

ثم بلوك الدالة factorial لحساب المضروب للوسيط المرسل باستخدام طريقة التكرات حيث في كل مرة يضيف الناتج الية ، ولان المقبض WRITER هو الافتراضي لكونة قد حدد مع select فأن خرج الدالة print تدخل الى الانبوب والتي في الاخير تقراء بواسطة عملية الأب ، وكذلك بلوك الدالة fibonacci نفسها مع اختلف اجراء الحساب فحسب .


ايضاً الدالة pipe تتعامل لإنشاء المقابض المتصل الى برنامج اخر بأكثر من طريقة ، والفكرة عموماً نقوم بتفرع عملية الأب بواسطة fork ومن خلال العملية بإعادة فتح واحد من المقابض الزوج بواسطة open اما STDIN أو STDOUT ثم البرنامج المطلوب بإستخدام الدالة exec :

pipe (READER,WRITER) or die "pipe no good: $!";
my $child = fork();
die "Can't fork: $!" unless defined $child;
if ($child == 0) { # child process
   close READER;              # child doesn't need this
   open (WRITER,">&STDOUT");  # STDOUT now goes to writer
   print exec $cmd,$args;
   die "exec failed: $!";
}
close WRITER;  # parent doesn't need this
print while <READER>;

في نهاية الكود فأن المقبض READER يربط الى الخرج القياسي للامر والتأثير المماثل له :

open (READER,"$cmd $args |") or die "pipe no good: $!";


Bidirectional Pipes

فكلتا الدالتين pipe و open لإنشاء مقابض ملفات ليست ذو اتجاهين unidirectional بمعني أذا رغبنا بالقراءة والكتابة معاً الى العملية لايعمل البرنامج وبشكل خاص فهذا الصيغة لاتعمل

open(FH,"| $cmd |");

لعمل ذلك هناك عدة طرق فأول طريقة بواسطة إستداعى الدالة pipe مرتين لإنشاء أزواج من المقابض المترابطة ، يستعمل الزوج الاول للكتابة من الأب الى الطفل والزوج الاخر للاطفال الى الأب ،والتالي برنامج لعبور الرسائلة ذهاباً وإياباً بين عملية الطفل والأب بإستدعى دالة pipe مرتين :

#!/usr/bin/perl
# message.pl 
use warnings;
use strict;

pipe CREAD, PWRITE;   # parent->child
pipe PREAD, CWRITE;   # child->parent

my $message = "S";

if (fork) {
    # parent--close child end of pipes
    close CREAD;
    close CWRITE;

    syswrite PWRITE, "$message \n";
    while (<PREAD>) {
        chomp;
        print "Parent got $_ \n";
        syswrite PWRITE, "P$_ \n";
        sleep 1;
    }
} else {
    # child--close parent end of pipes
    close PREAD;
    close PWRITE;

    while (<CREAD>) {
        chomp;
        print "Child got $_ \n";
        syswrite CWRITE, "C$_ \n";
    }
}

تصل كل العمليات الى مقابض الملفات بواسطة الأنابيب بعد التفرع وكل عملية تغلق المقبضين التي لان تحتاجهم وتقراء وتكتب من المقابض الاخرى ، ولكي يضمن عدم الجمود deadlock للتخزين المؤقت buffering تقوم العمليات بإنتظار لكل الرسائل الاخرى ، ثم الدالة syswrite لعمل الكتابة الفعلية التي تعفو من إستعمال العلم autoflush ، وعند تشغيل البرنامج فأن خرج :


Output

Child got : S
Parent got: CS
Child got : PCS
Parent got: CPCS
Child got : PCPCS
Parent got: CPCPCS
...




ويمكن ايضاًُ استعمال طريقة اسرع بإستخدام الواحـدات القياسية IPC::Open2 and IPC::Open3 لإنشاء مجموعة من المقابض المرتبطة بالـSTDIN, STDOUT, and STDERR للعمليات الفرعية ، واكثر طريقة محببة لإنشاء انابيب bidirectional بأستعمال الدالة socketpair التي تنشاء اثنين من المقابض المترابطة linked filehandles كما تفعل الدالة pipe ولكن بدلاً من أن يكون اتصال واحد يصبح المقابض لقراءة والكتابة ، والبيانات التي تكتب الى المقبض تخرج الى الاخر والعكس بالعكس ، نجد ان الدالة socketpair تتضمن نفس مفاهيم المقابس بالدالة socket التي تستعمل للإتصالات عبر الشبكة وسوف نشير اليها فيما بعد

حيث يرتبط المقابس بثلاثة اشياء اساسية وهي domains, types, protocols :
الدومين في البيرل اما Internet أو Unix ، والنوع اما ان يكون بالـstreaming socket أو datagram socket أو raw socket والبروتوكول يمكن ان يكون بروتوكول إتصال أو PF_INET أو PF_UNIX ، وهنا الشكل العام للدالة socketpair لإنشاء المقابض SOCK_A و SOCK_B :

$boolean = socketpair (SOCK_A,SOCK_B,$domain,$type,$protocol) 

على سبيل المثال هنا نسخة اخرى لبرنامج message المشاهد سابقاً بإستخدام زوج من المقابس :

#!/usr/bin/perl
# socketpair.pl
use warnings;
use strict;

use Socket;

socketpair PARENT, CHILD, AF_UNIX, SOCK_STREAM, PF_UNSPEC;

my $message = "S";

if (fork) {
    syswrite PARENT, "$message \n";
    while (<PARENT>) {
        chomp;
        print "Parent got: $_ \n";
        syswrite PARENT, "P$_ \n";
        sleep 1;
    }
} else {
    while (<CHILD>) {
        chomp;
        print "Child got : $_ \n";
        syswrite CHILD, "C$_ \n";
    }
}

ايضاً يمكن إغلاق المقابض للإب والطفل في عمليات الطفل والأب التي لان نحتاجهم ، ويمكن ايضاً إستعمال المقبس في إتجاة واحد direction بإستخدام الدالة shutdown لإغلاق اما الدخل أو الخرج :

shutdown CHILD, 1;    # make child read-only
shutdown PARENT, 0;   # make parent write-only

فالاختلاف بين close و shutdown ، الدالة close لإغلاق المقبس عن العمل ولانستطيع القراءة والكتابة من المقبس ، بينما shutdown تمنح السيطرة على أطفاء جزء من الإتصال ذو الاتجايين bidirectional ، اول وسيط هو المقبس المتصل والثاني يشير الى كيفية إطفاء الاتجاة بوضع أحد القيم الصحيحة ، بوضع في هذا الحقل القيمة 0 فسوف يغلق المقبس لمنع القراءة اكثر من ذلك ، والقيمة 1 تغلق المقبس للكتابة ، والقيمة 2 تغلق المقبس للقراءة والكتابة كليهما (كماهي الدالة close ) .


الأنابيب المميزة من المقابض

ستحتاج من حين الى اخر لإختبار المقبض للرؤيتة ما اذا كان مفتوح على الملف أو الأنبوب ومن تلك الخيارات المحتملة :

الخيار الوصف
-p مقبض الملف هو أنبوب pipe
-t مقبض الملف مفتوح على الطرفية terminal
-s مقبض الملف هو مقبس socket


بوضع الخيار –p سوف يعيد true اذا فتح مقبض الملف على الأنبوب

print "I've got a pipe!\n" if -p FILEHANDLE;

اماالخياران –t و –S هي أنواع خاصة من مقابض الملفات فأذا كان المقبض مفتوح على الطرفية ( نأفذة الاوامر ) فأن الخيار –t يعيد true فمثلاً لرؤية ان STDIN في البرنامج يشغل بشكل تفاعلي أو انة قد حول بواسطة التوجية الى ملف :

print "Running in batch mode, confirmation prompts disabled.\n"
      unless -t STDIN;

–S هي لمقابض الملف المفتوحة لمقبس الشكبة

print "Network active.\n" if -S FH

للقراءة اكثر حول خيارات اختبار مقابض الملفات كالحجم وتأريخ التعديل والملكية ومعلومات اخرى شاهذ صفحة perlfunc



خطأ الأنبوب

عندما يقراء برنامجنا من مقبض الملف المفتوح على الأنبوب و الاخر في النهاية وذلك اما بالخروج exits أو بإغلاق نهاية الأنبوب ، فسوف يستلم برنامجنا EOF لمقبض الملف ، التي تحدث بشكل معاكس عند الكتابة الى مقبض الملف المفتوح على الأنبوب ،فعندما يكتب برنامجنا إلى الأنبواب والبرنامج الاخر في النهاية وإنتهاءة قبل الأوان أو بإغلاق نهاية الإتصال ، فسوف يستلم البرنامج EOF لمقبض الملف .

البرنامجين اسفل الاول المسمى write_ten.pl يقوم بفتح الأنبوب الى البرنامج الاخر المسى read_three.pl ويحاول كتابة عشرة اسطر من النصوص الية ، فيفحص البرنامج الاول ناتج الكود لدالة print ويعمل زيادة للمتغير $count حينما تعيد الدالة print الناتج true ، وعندما يشغل write_ten.pl فأن محتوى المتغير $count يشير الى عدد الاسطر التي نحجت بالكتابة الى الأنبوب ، ويقراء برنامج read_three.pl الثلاثة السطور للنصوص من الدخل القياسي وثم يخرج exits من البرنامج كما هو مشاهد .

#!/usr/bin/perl
# file: write_ten.pl

use strict;
open (PIPE,"| perl read_three.pl") or die "Can't open pipe: $!";
select PIPE; $|=1; select STDOUT;

my $count = 0;
for (1..10) {
  warn "Writing line $_\n";
  print PIPE "This is line number $_\n" and $count++;
  sleep 1;
}
close PIPE or die "Can't close pipe: $!";

print "Wrote $count lines of text\n";

لاحظ وضع pipe الى النمط autoflush لذا فأن اي نص يكتب اليه يرسل الى الأنبوب مباشرتاً بدلاً من التخزين المؤقت في buffer المحلي ، ثم وضع الدالة sleep (عمل halt مؤقت لإيقاف التنفيذ في المعالج ولكي تصبح جاهزة بعد انقضاء الوقت ) لثانية واحدة بعد كتابة كل سطر من النصوص والذي يعطى الى برنامج read_three.pl الذي يستلم التقرير من النصوص

#!/usr/bin/perl
# file: read_three.pl

use strict;
for (1..3) {
  last unless defined (my $line = <>);
  warn "Read_three got: $line";
}

عند تشغيل السكربت write_ten.pl الخرج :


Output

% write_ten.pl
Writing line 1
Read_three got: This is line number 1
Writing line 2
Read_three got: This is line number 2
Writing line 3
Read_three got: This is line number 3
Writing line 4
Broken pipe
%


فكل شيء يعمل كما هو متوقع منه خلال الثلاثة الاسطر ولكن عند الخروج من التكرار فلاشيء سوف يقراء ، وعند محاولة write_ten.pl كتابة بقية الاسطر فسوف يتحطم البرنامج مع ظهور الخطأ Broken pipe ، حيث ان البيانات التي تطبع عدد الاسطر تعبر بنجاح الى الأنبوب لكنها لاتنفذ .

عند محاولة البرنامج الكتابة الى الأنبوب والبرنامج الاخرى لايقراءه الى النهاية فهذا يؤدي الي مايعرف بالإستثناء الأنبوب PIPE exception ، يستلم الأشارة PIPE signal في هذا الاستثناء إلى الكأتب ، والافتراضي لهذة الإشارة تؤدي الى إنهاء مباشر للبرنامج المعأب، ويظهر نفس هذا الخطأ في تطبيقات الشبكة عندما المرسل يحاول إرسال بيانات الى البرنامج البعيد الذي يكون قد خرج أو تؤقف عن الإستلام .



الإشارات Signals

الإشارات هي آلالية الأساسية لنظام التشغيل للإشارة الى شروط الخطأ والإحداث الغير متوافقة زمنياً asynchronous وتحدث في اوقات عشوائية غير متزامن للعملية ، فبشكل خاص تستعمل للتعامل مع برامج الشبكه ، فترسل الإشارة رسالة أو نبضات الى برنامجك بواسطة نظام التشغيل لتخبره بوجوب حدوث هذا الحدث ، ويمكن ان تشير الإشارة الى خطأ ما في البرنامج نفسه مثل محاولة القسمة على الصفراو حدث للمعلومات غير حرجة كالإنتهاء العملية الفرعية في برنامجك المشغل حالياً أو قد يقوم المستخدم بطلب اعتراض البرامج .

ترسل الإشارات بواسطة نظام التشغيل حيث ان العمليات يمكن ان تشير الى بعضها البعض ،فعلى سبيل المثال عندما تضغط على ^C control+C من على لوحة المفاتيح فاننا نقوم بإرسال إشارة الاعتراض interrupt signal للبرنامج المشغل ، وتلك الإشارة لان ترسل الى نظام التشغيل انما ترسل الى عملية الشل التي تترجم ضربات المفاتيح ،ومن الممكن ايضاً إرسال إشارة للعملية نفسها .



الإشارات المشتركة:

يعرف معيار POSIX القياسي تسعة عشر إشارة ولكل إشارة من هذه الإشارات قيمة صحيحة وإسم يرمز له في الجدول اسفل ( لا تمثل مجموعة الاعداد الصحيحة الـ gaps ضمن الإشارات القياسية المشتركة والموحدة في أنظمة التشغيل ).

يشير العمود الثالث من الجدول إلى ما الذي سوف يحدث عندما العملية تستلم الإشارة ، فبعض الإشارات لاتعمل شيء والبعض تسبب بإنهاء العملية مباشرتاً والبعض تسبب بإنهاء العملية وعمل core dump ( ملف core يقوم نظام التشغيل بإظافتة حينما يحدث خطأ في زمن تحطيم البرنامج وقد يصل الى الحجم 10MB في المجلد الحالي للبرنامج ، ويفيد هذا الملف للتنقيح وإيجاد سبب الفشل للقيم والمتغيرات والبيانات وواحد من اكثر الإسباب المسببة لذلك التحطيم الخطأ المشترك segmentation fault) ، واكثر الإشارات يمكن ان تمسك "caught" لذلك من الممكن للبرنامج ان يحمل قائد handler للإشارة لعمل حدث خاص فيه عندما الإشارة تستلم ،مع ذلك فأن بعض الإشارات لايمكن ان تعترض بهذة الطريقة كذلك لانحتاج الى فهم القائمة كلها في الجدول بسبب ان اغلبهم غير متوقع حدوثهم خلال تنفيذ برنامج البيرل وبشكل اوضح تشير الى low-level bug في البيرل نفسها ، و الأكثر استخداماً لقائدة الإشارات الموحدة سنرها في التالي :



Table POSIX Signals:

Default action Comment Notes Value Signal Name
terminate Hangup detected A 1 HUP
terminate Interrupt from keyboard A 2 INT
terminate+core Quit from keyboard A 3 QUIT
terminate+core Illegal Instruction A 4 ILL
terminate+core Abort C 6 ABRT
terminate+core Floating point exception C 8 FPE
terminate Termination signal AF 9 KILL
terminate User-defined signal 1 A 10 USR1
terminate+core Invalid memory reference C 11 SEGV
terminate User-defined signal 2 A 12 USR2
terminate Write to pipe with no readers A 13 PIPE
terminate Timer signal from alarm clock A 14 ALRM
terminate Termination signal A 15 TERM
ignore Child terminated B 17 CHLD
continue/ignore Continue if stopped E 18 CONT
stop process Stop process DF 19 STOP
stop process Stop typed at tty D 20 TSTP
stop process tty input for background process D 21 TTIN
stop process tty output for background process D 22 TTOU

Notes: A—Default action is to terminate process. B—Default action is to ignore the signal. C—Default action is to terminate process and dump core. D—Default action is to stop the process. E—Default action is to resume the process. F—Signal cannot be caught or ignored.

فكل إشارة لديها حدث إفتراضي لها ولاحظ ان اكثرها تقوم بالإنهاء .




إمساك الإشارات

نستطيع الإمساك بالإشارة وذلك بإظافة قائد الإشارة الى الهاش العام %SIG ونضع فية أسم الإشارة المرادة للمسك كمفتاح الهاش مثلاً $SIG{INT} وذلك للحصول أو لتعين قائد الإشارة INT ، أما لقيمة الهاش نستخدم مرجع code فأما ان تكون دالة مخفية أو مرجع الى أسم الدالة ، فعلى سبيل المثال السكربت أسفل يقوم بتحميل القائد INT ، وبدلاً من ان ينتهي البرنامج عندما نضغط على مفتاح الإعتراض ^C سيقوم بطباعة رسائلة ويزيد (bumps up ) عداد المسجل ويستمر الإرسالات وعند ثلاثة تنتهي العملية

#!/usr/bin/perl
#file:interrupt.pl
use strict;
my $interruptions=0;
$SIG{INT}=\&handle_interruptions;

while($interruptions < 3) {
  print "I'm sleeping.\n";
  sleep(5);
}

sub handle_interruptions {
  $interruptions++;
  warn "Don't interrupt me! you've already interrupted me
 ${interruptions}x.\n";
}

سنخبر النواة لإستدعاء الدالة التي اعداءدنها حينما الإشارة تحدث ونستطيع مثلاً في هذا الدالة معالجة الشرط فيما بعد

الخرج :


Output

% interrupt.pl
I'm sleeping.
I'm sleeping.
Don't interrupt me! You've already interrupted me 1x.
I'm sleeping.
I'm sleeping.
Don't interrupt me! You've already interrupted me 2x.
I'm sleeping.
Don't interrupt me! You've already interrupted me 3x.



شرح الكود :

قمنا بتعريف المتغير $interruptions وجعلة عام في البرنامج حيث نستخدمة كاعداد لعدد مرات إستلام INT ، ثم تحميل القائد INT فقمنا بتحميل قائد الإشارةINT بواسطة وضع $SIG{INT} الى مرجع الى الدالة handle_interruptions،ثم في التكرار لطباعة الرسائلة وإستدعي الدالة sleep بتحديد خمس ثواني ويستمر حتي يصبح $interruptions بثلاثة أو اكبر بينما الدالة handle_interruptions تستدعى حينما تصل الإشارة INT ثم تطبع الرسائلة وحتي وأن كان البرنامج مشغول بشيء اخر في تلك اللحظة فأن قائد الإشارة يزيذ $interruptions ويطبع رسائلة التحذيرة التي وضعنا .

بنفس العمل يمكن ان نستخدم الدالة المخفية للإختصار بإظافة قيمة الهاش بواسطة مراجع كود لإستداعى الدالة حينما تحدث الإشارة

$SIG{INT} = sub {
                $interruptions++;
                warn "Don't interrupt me! You've already interrupted me
 ${interruptions}x.\n";
                 };

الهاش %SIG ايضاً يميز بين حالتين : فعند وضع النصوص "DEFAULT" لكي تعيد تخزين السلوك الإفتراضي للإشارة ، فمثلاً عند وضع $SIG{INT} بالقيمة " DEFAULT " فسوف يسبب لإشارة INT الى إنهاء السكربت ، وكذلك النصوص " IGNORE " لتجاهل كل إشارة تحدث

$SIG{INT}="IGNORE";

وكما ذكر سابقاً فأن القائدة KILL و STOP لإيمكن أن تمسك أو تهمل والحدث الإفتراضي لهما دائماً سوف ينجز ، وأن رغبت في استعمال نفس الدالة للإمساك مع مختلف الإشارات فمن المهم تميز الدالة لكل إشارة عن الاخرى ونستطيع فعل ذلك بإلنظر الى أول وسيط الذي يحوي على أسم الإشارة ، فمثلاً في حالت إرسالت إشارات INT فأن القائد سيكون مسمى بالنصوص " INT " :

$SIG{TERM} = $SIG{HUP} = $SIG{INT} = \&handler
sub handler {
   my $sig = shift;
   warn "Handling a $sig signal.\n";
}


قيادة إستثناءات الأنبوب (Handling PIPE Exceptions)

لتعامل مع إستثناءات PIPE بالعودة الى البرامج السابقة write_ten.pl الذي يفتح الأنبوب للبرنامج read_three.pl ويحاول كتابة عشر اسطر الية لكن في برنامج read_three.pl مستعد لأخذ ثلاثة سطور فقط وبعد الخروج من التكرار يغلق نهايتة للإنبوب ،والبرنامج write_ten.pl لايعرف بأن نهاية الإتصال بينهما عند محاولة كتابة رابع سطر ويولد إشارة PIPE .

يمكننا تعديل البرنامج write_ten.pl لإكتشاف خطأ PIPE و تصحيح بسهوله ويسر مع مختلف الطرق ، على سبيل المثال في الاسكربت اسفل وضع المتغير $ok بالقيمة true ثم تحميل القائد PIPE وعند إستلام إشارة PIPE فأن القائد سوف يخصص المتغير $ok بقيمة غير معرفة .

#!/usr/bin/perl
# file: write_ten_ph.pl

use strict;

my $ok = 1;
$SIG{PIPE} = sub { undef $ok };

open (PIPE,"| perl read_three.pl") or die "Can't open pipe: $!";
select PIPE; $|=1; select STDOUT;

my $count = 0;
for ($_=1; $ok && $_ <= 10; $_++) {
  warn "Writing line $_\n";
  print PIPE "This is line number $_\n" and $count++;
  sleep 1;
}
close PIPE or die "Can't close pipe: $!";

print "Wrote $count lines of text\n";

وعند تشغيل البرنامج المعدل :


Output

% write_ten_ph.pl
Writing line 1
Read_three got: This is line number 1
Writing line 2
Read_three got: This is line number 2
Writing line 3
Read_three got: This is line number 3
Writing line 4
Wrote 3 lines of text



الطريقة الاخرى بوضع قيمة $SIG{PIPE} بالنصوص 'IGNORE' لتجاهل إشارة الإنبوب كلياً ووضع شرط لفحص ناتج الدالة print وإن اعادت false فسوف يخرج من التكرار .

#!/usr/bin/perl
# file: write_ten_i.pl

use strict;

$SIG{PIPE} = 'IGNORE';

open (PIPE,"| perl read_three.pl") or die "Can't open pipe: $!";
select PIPE; $|=1; select STDOUT;

my $count=0;
for (1..10) {
  warn "Writing line $_\n";
  if (print PIPE "This is line number $_\n") {
    $count++;
  } else {
    warn "An error occurred during writing: $!\n";
    last;
  }
  sleep 1;
}
close PIPE or die "Can't close pipe: $!";

print "Wrote $count lines of text\n";

وعند تشغيل السكربت :


Output

% write_ten_i.pl
Writing line 1
Read_three got: This is line number 1
Writing line 2
Read_three got: This is line number 2
Writing line 3
Read_three got: This is line number 3
Writing line 4
An error occurred during writing: Broken pipe
Wrote 3 lines of text



لاحظ ان رسائلة الخطا تظهر في المتغير $! بطبعة "Broken pipe" واذا اردت اختبار قيمة الخطأ من I/O errors اخرى بشكل واضح عن طريق نمط المطابقة أو الافضل فحص القيمة العددية الثابتة للخطأ EPIPE حيث لكل خطأ له قيمة معينة ،على سبيل المثال

use Errno ':POSIX';
...
unless (print PIPE "This is line number $_\n") { # handle write error
   last if $! == EPIPE;   # on PIPE, just terminate the loop
   die "I/O error: $!";   # otherwise die with an error message
}


إرسال الإشارات

يرسل اسكربت البيرل الإشارة الى عملية اخرى بإستخدام الدالة kill

الصيغة :

$count = kill($signal,@processes)

ترسل الدالة الإشارة $signal الى العملية أو اكثر من عملية ، يمكن تحددها عددياُ مثل 2 أو بالإسم مثل " INT " ، وتخزن قائمة IDs العمليات في المصفوفة @processes لتسليم الإشارة اليها ، فإن نجحت عدد من إشارات العمليات تسترجع ذلك العدد في الناتج .

تكفي عملية واحدة للإشارة الى العمليات الاخرى والتي لديها الإمتيازات الكافية لفعل ذلك ، فيستطيع المستخدم العادي الإشارة الى العمليات الاخرى التي تشغل بنفس إمتيازات المستخدم ، والعملية المشغلة مع الروت تستطيع الإشارة الى إي عملية اخرى .

تنجز الدالة kill بعض الخدع فأذا استعملت الإشارة $signal المحددة ذات الرقم صفر فهي لان ترسل إشارة الى العملية وتعيد الدالة kill عدد العمليات التي إشيرت بدون تسليم الإشارة وهذة الطريقة عادتاً تستعمل لفحص عملية الطفل ما اذا كانت حية ومن غير UID مغير، وأن استخدمت عدد سالب للـ process ID فهي تقتل عملية المجموعة بدلاً من العمليات حيث ان الدالة kill ستعالج القيمة المطلقة من العدد اي المقابل لعدد السالب بالموجب كال process group ID وتسليم الإشارة لكل من في المجموعة ، فمثلاً عندما نستخدم الأنبوب pipe في شل اليونكس لإمر الى اخر ، نبتدأ من عمليتين ولكن من عمل job واحد ، وعند الضغط على Ctrl-C لإعتراض العمل الحالي ، فذلك يرسل الإشارة الملائمة الى العمل كله الذي قد يكون اكثر من عملية .

kill  9     => $pid;                    # send $pid a signal 9
kill -1     => $pgrp;                   # send whole job a signal 1
kill  USR1  => $$;                      # send myself a SIGUSR1
kill  HUP   => @pids;                   # send a SIGHUP to processes in @pids

وبإمكان إرسال الإشارة الى نفس العملية بإستدعى الدالة مع المتغير $$ فمثلاً:

kill INT => $$; # same as kill('INT',$$)


تحذير من قائد الإشارة

قد تصل الإشارة في أي وقت أثناء تنفيذ البرنامج ، فقد تصل بينما مفسر البيرل يعمل أشياء مهمة كعمل تحديث أوتعديل لتركيب ذاكرتة ،وحتى وأن كانت داخلة باستدعاءات النظام system call كالنوم بواسطة sleep ، فإذا كان قائد الإشارة يعمل شيئاً للذاكرة المرتبة مثلاً كالتوطين allocating أو بترتيب تركيب بياناتة الكبيرة فقد تجد تغير كاملاً عند العودة من القائد ، فتحصل البيرل بطبعتها على تشويش التي تؤدي إلى تحطم النتائج من حين لآخر .

ولتجنب هذة الإمكانية يجب على قائدة الإشارات أن تشغل على أقل الإحتمالات ولذلك يجب التعقب من الأمن بوضع متغير عام وإعادتة كما تم في السابق في المثال لسكربت write_ten_ph.pl ، وبالإضافة عند التعامل مع تغيرات الذاكرة فأن عمليات I/O بضمن قائدة الإشارة يجب ان تتجنب بالرغم من التخصيص مع الكلمة المحجوزة warn في اكثر من حالة الا ان هذة التعاملات يجب ان تجرد خارج بلوك قائد الإشارة في برامج الإنتاج .

ونستطيع ان نستدعى عموماً die و exit ضمن قائدة الإشارة ولكن الإستثناءات على بئية الونيدوز تسبب بالتقيدات لمكتبة الإشارة وعند تضمن الدالتين للقائد الإشارة يسبب بالخطأ "Dr.Watson" .

فالحقيقة من تضمين الإشارات في بيئة الونيدوز محددة جداً في الوقت الحاضر فمثلاً الإشياء السهلة كالقائد INT لمسك مفتاح الإعتراض فسوف يعمل ، لكن الإشياء الاكثر تعقيداً مثلاً قائدة CHLD لمسك العملية الفرعية الميتة فهو لان يعمل أطلاقاً، وهذا الشيء يجب من التأكد منها عند التطوير من خلال فحص المرحل release منها قبل محاولة الكتابة أو عند بناء اي كود معتمد بشكل أساسي على الإشارات .



Timing Out Slow System Calls

قد تحدث الإشارة بينما البيرل تنفذ اي من إستدعاء النظام system call وفي الغالب البيرل سوف تعيد من تشغيل restarts الإستدعاء بشكل آلي بعد الفترة المؤافقة بالضبط حينما تترك ،حيث ان لبعض إستدعاءات النظام هي إستثناءات كالدالة sleep والدالة select التي هي باربعة وسطاء ، وعند استعمال sleep لتعليق ونوم البرنامج لبعض من الثواني فهي على اي حال ستخرج مبكراً وتعيد عدد الثواني التي نامت قبل أن تصحى ، وهذة الدالة مفيدة لكي يتوافق البرنامج لحدوث بعض الإحدث التي نتوقعها

الصيغة :

$slept = sleep([$seconds]) 

النوم بالعدد المشار في الثواني أو الى أن تستلم الإشارة ،وتعيد عدد الثواني التي نامت فعليلاً في البرانامج ، وان لم يكن لديها اي وسيط فسوف تنام الى مالانهاية .

الطريقة الاخرى للإستثناء من خلال الدالة select مع اربعة الوسطاء حيث يمكنها لإداء الإنتظار حتى تصبح واحد أو اكثر من مجموعة مقابض الملفات مستعدة ready للخرج والدخل I/O سنتطرق اليها فيما بعد .

احياناً عند إعادة تشغيل بواسطة إستدعاءات النظام بشكل آلي ليس ما نرغب بة فمثلاً بإعتبار ترك المستخدم الجهاز ولم يقم بإدخال كلمة السر والبرنامج في هذا الوقت ينتظر قراءة الرد من الدخل القياسي في التنفيذ ، فقد نريد القراءة لمدة محددة ثم الخروج time out بعد الفترة الزمنية المحددة في حال كون المستخدم غير مؤجود على الجهاز في هذة الاثناء وقد ترك الطرفية منذ مدة زمنية ، وللتوضيح المثال التالي:

my $timed_out = 0; 

$SIG{ALRM} = sub { $timed_out = 1 };

print STDERR "type your password: ";
alarm (5);    # five second timeout
my $password = <STDIN>;
alarm (0);    # cancel alarm

print STDERR "you timed out\n" if $timed_out;

تستخدم الدالة alarm لوضع مؤقت أو منبة للوقت ، وعندما ينتهي المؤقت يولد نظام التشغيل إشارة ALRM وتمسك الإشارة ويتم تنفيذ كود الدالة المخفية بوضع المتغير $timed_out الى القيمة 1 عند إستلام الإعتراض ،ونرى وضع المنبة بخمس ثواني للزمن الخروج timeout ثم قراءة سطر من الدخل القياسي STDIN وبعد أكمال القراءة تستدعى الدالة alarm مرى اخرى بالقيمة 0 وذلك لإعادة المؤقت الى off أو للإلغاء المنبة عن العمل ، حيث يعطى المستخدم 5 ثواني للطباعة كلمة السر واذا لم يتجاوز 5 الثواني فسوف يطفئ ساعة المنبة ثم تكملة بقية البرنامج .

الصيغة :

$seconds_left = alarm($seconds) 

تعدل إشارة ALRM لتكون مؤلدة الى العملية بعد الثواني المحددة $seconds ، وناتج الدالة هو رقم الثواني المنجزة من المؤقت السابق ، وأن أعطي المؤقت بالقيمة 0 فذلك يعني عدم تمكين المؤقت .

المشكلة من إعادة التشغيل بشكل آلي في البيرل بطيئة لإستدعاء النظام المتضمنة مع <> بالرغم من أن ساعة المنبة تطفئ الا ان إستدعاء <> تبقى في إنتظار المستخدم لكي يدخل البيانات ، واذا كانت الرغب بعد الإنتظار بعد إنقضاء الوقت المحدد للدخل نستعمل eval{} وقائد الإشارة ALRM المحلي لإجهاض عملية القراءة والتعبير العام لذلك :

print STDERR "type your password: ";
my $password =
  eval {
    local $SIG{ALRM} = sub { die "timeout\n" };
    alarm (5);    # five second timeout
    return <STDIN>;
  };
alarm (0);
print STDERR "you timed out\n" if $@ =~ /timeout/;

بلوك eval يتضمن قائد ALRM المحلى والدالة المنبة alarm كمافي السابق عند المحاولة للقراءة من STDIN فأذا إعدت قيمة من <> قبل أنقضاء المؤقت فسوف تخصص القيمة العادة الى المتغير $password .

عند انقضاء الوقت قبل الإكمال من عملية الإدخال فسوف ينفذ القائد ALRM ويمؤت مع رسائلة الخطأ "timeout."، على اية حال لكون أنها تموت بضمن بلوك eval ، فالتأثير منها لان يعيد undef وتوضع الى المتغير $@ للرسائلة الخطأ الأخيرة ، ويمكنا عمل مطابقة للنمط للمتغير الخاص $@ لرسائلة الـtimeout وطباعة نوع التحذير أن وجد .

اما في حالة إطفاء المؤقت مباشرتاً بعد العودة من بلوك eval بإستعمال القيمة صفر لكي يتجب المؤقت في الحظات الغير مناسبة .


معالجة الأطفال وحصاد اكواد الخروج

عندما الطفل يخرج فأن نظام التشغيل يحتفظ بالسجل حالة خروجة وتبقى عملية الطفل في جدول العملية process table، وتبقى حالة الخروج حتي يحصدهم الأب ، وبدون عمل ذلك فأن الإطفال الموتة تعاد الى مايعرف بالـ zombies عندما يخرج الأب وتعلق في جدول العملية حيث يبقى المؤتة غير مدفؤنين

الامر ps لمشاهد حالة العلمية من مداخل الجدول اذا كانت زومبياً zombie (أو مباد defunct) .


إنتظار طفل وحيد

لإحصاد حالة الخروج نستخدم الدالة wait أو الدالة waitpid الاكثر تحديداً للطفل ، فالإجبار عملية الأب الى إنتظار الطفل من الإنتهاء قبل الإستمرار ، فأنة ليس هنالك مشكلة من إستخدام الدالة wait لإيقاف تفيذ الأب حتي خروج الطفل :

# fork a child and execute an external command in it
exec @command unless fork;

# wait for the child to exit
$child_pid = wait;

تعيد الدالة wait الـprocess ID من الطفل ، و بدلاُ من ذلك يمكن إيجادة في المتغير الخاص $?

على سبيل المثال عند تفرع عملية وحيدة من الأب

#!usr/bin/perl 
print "PID=$$\n";
my $n;
print "fork program starting\n";
my $child=fork();
die "can't fork:$!"unless defined $child;

if($child == 0) {  print "child process: PID=$$\n";
$message = "This is the child";
$n = 6;
}
else {
print "parent process:PID=$$\n";
$message = "This is the parent";
$n = 3;
}

for(; $n > 0; $n--) {
print $message,"\n";
sleep(1);
}

الطفل يطبع ست رسائل بينما الأب يطبع ثلاث فقط ، فعملية الأب تنتهي قبل الطفل لذلك نقوم بوقف تنفيذ الأب بواسطة الإنتظار wait وحتي خروج الطفل

# wait for the child to exit
$child_pid = wait;
print "$child_pid\n";

لكن ماذا ان الأب أطول عمراً من الطفل بتعديل بسيط في المثال السابق

#!usr/bin/perl 
print "PID=$$\n";
my $n;
print "fork program starting\n";
my $child=fork();
die "can't fork:$!"unless defined $child;
if($child == 0) {  print "child process: PID=$$\n";
$message = "This is the child";
$n = 3;
}
else {
print "parent process:PID=$$\n";
$message = "This is the parent";
$n = 6;
}
for(; $n > 0; $n--) {
print $message,"\n";
sleep(1);
}

@a=exec "ps -al";
print @a,"\n";

فعند خروج عملية الطفل يرسل الأب إشارة CHLD بواسطة النواة وعملية الطفل تصبح zombie حتي يستدعى الأب الدالة wait أو waitpid ،وبخروج الأب فأن مفسر البيرل يقوم بحصاد الإطفال zombied ، ولكن ان استخدمنا تفرع fork جديد اخر في المثال السابق يجب ان تقوم بتظيف العمليات بنفسنا في العديد من النواة (ليس في جميع النواة) ، ونسطيع من إعداد مسبق للإشارة للحصاد آلي autoreaping zombies بواسطة وضع قائد الإشارة $SIG{CHLD} الى 'IGNORE'


Output

% perl zombie.pl
PID=5923
fork program starting
child process: PID=5924
This is the child
parent process:PID=5923
This is the parent
This is the child
This is the parent
This is the child
This is the parent
This is the parent
F S   UID   PID  PPID  C PRI  NI ADDR SZ WCHAN  TTY    CMD
0 S  1000  5923  5742  0  80   0 -  1175 -      pts/0  perl
0 R  1000  5924  5923  0  80   0 -   607 -      pts/0  ps
This is the parent
This is the parent
F S   UID   PID  PPID  C PRI  NI ADDR SZ WCHAN  TTY    CMD
0 R  1000  5923  5742  0  80   0 -   606 -      pts/0  ps
0 Z  1000  5924  5923  0  80   0 -     0 -      pts/0  ps 



معالجة عمليات الطفل العديدة :

اذا لدينا اكثر من عملية فعند استخدام الدالة wait فهي لاتكفي لانها سوف تعيد عندما اي طفل يخرج ، ولإنتظار طفل محدد نستخدم الدالة waitpid :

$pid = fork;
if ($pid) {
waitpid $pid, 0;
} else {
...child...
}

فاول وسيط للدالة waitpid هو process ID للطفل المنتظر الية ، وثاني وسيط هو العلم الذي يؤثر على وظيفة الـwaitpid والغالب يوضع الى العلم المشترك WNOHANG التي تخبر الدالة waitpid بأن لا تنتظر الطفل اذا مازال يشغل لكن يعاد مباشراً -1 ، وهذا الوسيط واحد من العديد من الثوابت المعرفة بالموديل POSIX ويمكن إستيراد اما من مجموعة sys_wait_h أو مباشرتاً :

use POSIX qw(:sys_wait_h);
Or:
use POSIX qw(WNOHANG);

ونستطيع بهذا التحقق والتدقيق لخروج الطفل بدون أجبار للإنتظار لها

$pid = fork;
if ($pid) {
while (waitpid $pid, WNOHANG) == -1) {
print "Waiting for $pid...\n";
sleep 5;
}
} else {
...child...
}


مشاركة البيانات بين العمليات

كما لاحظنا سابقاً من المشاكل المواجهة للعمليات المتفرعة في عملية التفرع وسنلاحظ مرة اخرى في الخيوط threads من الصعوبة مشاركة البيانات للإتصال مع بعضهم البعض حيث تحتاج العمليات الى أما بتثبت قناة للإتصال أو إيجاد بعض النقاط من المرجع المشتركة التي يمكن رويتها بواسطة كل العمليات المتعلقة بها ، في هذا الجزء نناقش وسائل IPC المنجزة بواسطة إنظمة يونكس والتي تعرف System V IPC يشير الحرف V الى "Very High Speed" عادتاً

هناك ثلاثة مكؤنات لنظام SysV IPC وهم :

  1. رسائلة الطوابير message queues

  2. السيمافورات semaphores

  3. أقسام ذاكرة المشاركة shared memory segments .


فنظام SysV IPC جزء ثابت في إنظمة اليونكس وعموماً غير قابل للنقل في أرصفة غير اليونكس وبشكل محدد في الأرصفة التي لاتستجيب لمعاملات POSIX ، ومازالت تستعمل بعض الإجزاء مع الأنابيب و أزواج المقابس socketpair وذلك بسبب أن الكائنات بشكل خاص يمكن ان تنشاء ونصل اليها بشكل دائم في الذاكرة ويمكن ان ينجو التطبيق من الموت وهذا يسمح للتطبيق من تخزين كل مهمات بياناتة الحرجة في ذاكرة المشاركة والتقاط ماهو مطلوب بالتمام ويتخلى عنه اذا إنتهاء التطبيق أو أذا أعيد تشغيلة restarted .

تتفاعل أغلب أجزاء البيرل إلى مكتبة lower-level ويتطلب نظام IPC عدداً من الثوابت التي تعرف الوسطاء المختلفة المطلوبة بواسطة دوالة خاصة ، وثوابت الـ IPC معرفة من قبل الموديل المسماة IPC::Sys في البيرل لذا بالعادة فإي تطبيق يستخدم IPC يتضمن السطر التالي :

use IPC::SysV;

فليس بالفعل لإستعمال IPC أن يكون نظام V على ارصفة اليونكس لكن يعمل مع الارصفة التي تدعم IPC المطلوبة في التطبيق على اي حال البيرل لن تقوم بتنزيل الوحـدات إن لم تكن IPC متؤفرة ولفحص اذا كانت IPC متؤفرة نستعمل الامر :

% ipcs

فأن كانت IPC متؤفرة فسينج تقرير لكلاً من shared memory segments, semaphores, and message queues الموجودة . فالشيء المشترك بين مكؤنات IPC الثلاثة جميعهم يستقرون في الذاكرة ويمكن الوصول اليها بوسطة إي عملية التي تعرف ID المصدر ولها ميزات الوصول ، وهذا مايميزها عن باقي إستراتيجيات IPC الاخرى ، التي تقوم بتثبت خطوط أو سطور private للإتصال بين العمليات والتي لايمكن الوصول اليها بسهولة بالعمليات الاخرى الغير مرتبطة .

على وجة التحديد فأن تدعيمات IPC تؤفر العديد من الثوابت و إستدعئات الدوال (كالإستدعاءت msgctl, msgget, msgrcv, msgsnd, semctl, semget, semop, shmctl, shmget, shmread, and shmwrite ، ) في اللغة التي هي مغلفة للإستدعئات السي المكافئة ، لكون هذة الدوال ليست سهلة الاستعمالات فأن لدى البيرل عائلة IPC:: من الوحـدات المتضمنة والمدعمة بواسطة الكائنات الموجهه للوصول الى نظام المخاطبة IPC .



IPC::SysV

وحـدة IPC::Sys تقوم بإستيراد كل ثوابت SysV المحددة لإستخدمها في برنامجنا المرتبط مع وحـدات IPC الاخرى ، والجدول اسفل لإغلب الاستخدامات الموسعة للثوابت

الثابت الغرض
GETALL تعيد مصفوفة لجميع القيم في مجموعة السيمافور
GETNCNT تعيد عدد العمليات المنتظرة التي تنتظر زيادة قيمة السيمافور المحدد في المجموعة
GETPID تعيد الـPID للعملية الأخيرة التي أنجزت التعليمة على السيمافور المحدد في المجموعة
GETVAL تعيد قيمة السيمافورالمحدد في المجموعة
GETZCNT تعيد عدد العمليات المنتظرة لسيمافور محدد في المجموعة ليصبح صفراً
IPC_ALLOC Currently allocated
IPC_CREAT إنشاء مدخل entry أذا المفتاح غير موجود
IPC_EXCL الفشل أذا كان المفتاح موجود
IPC_NOERROR إزالة الرسالة ومسحها من الطابور إذا كانت أطول من حجم buffer القراءة (عادتاً تبقي الرسالة ويعود بخطأ )
IPC_NOWAIT الخطأ إذا كان الطلب request يجب أن يوضع في الإنتظار
IPC_PRIVATE المفتاح الخاص
IPC_RMID يستخدم لإزالة المصدر(طابور الرسائلة أو السيمافور)
IPC_SET يستخدم لوضع خيارات المصدر
IPC_STAT يستخدم لحصول على خيارات المصدر
IPC_W كتابة أو أرسال التصريح
MSG_NOERROR نفس IPC_NOERROR
SEM_UNDO أرجاع وظيفة السيمافور السابقة في حالتة قد خرجت من العملية process exits
SETALL وضع القيمة لجميع قيم السيمافورات في المجموعة إلى القيمة المحددة
SETVAL وضع قيمة لسيمافور إلى القيمة المحددة
S_IRUSR 00400: صلاحية القراءة للمالك
S_IWUSR 00200: صلاحية الكتابة للمالك
S_IRWXU 00700 : صلاحية (قراءة/كتابة/تنفيذ) للمالك
S_IRGRP 00040: صلاحية القراءة للمجموعة
S_IWGRP 00020: صلاحية الكتابة للمجموعة
S_IRWXG 00070: صلاحية (قراءة/كتابة/تنفيذ) للمجموعة
S_IROTH 00004: صلاحية القراءة لمستخدمين الاخرين
S_IWOTH 00002: صلاحية الكتابة لمستخدمين الاخرين
S_IRWXO 00007: صلاحية (قراءة/كتابة/تنفيذ) لمستخدمين الاخرين
ftok تحويل pathname و process ID الى معرف key_t-type SysV IPC (حيث ان مفتاح السيمافور Key semaphore هو قيمة صحيحة يستخدم لسماح لعمليات الغير مرتبطة من الوصول إلى نفس السيمافور، ونصل الى جميع السيمافورات بشكل غير مباشر بتزويد البرنامج بالمفتاح ثم بعد ذلك يقوم النظام بتوليد معرف السيمافور لتعامل معه )



Messages Queues

فيما مضى رسالة الطوابير كانت هي الطريقة الوحيدة الفعالة والكفوء لإتصال بين العمليات وذلك عن طريق إستخدام برنامج طابور بسيط لدخول العمليات الية بحيث تكتب الرسائل من جهه وقراءتها من الجهه الاخرى

رسالة الطابور في الحقيقة هي قوائم متربطة linked list من الرسائل المخزنة ضمن النواة ويعرف بواسطة معرف يسمى بالـ message queue identifier ، وفي هذا الجزء سأضع كلمة الطابور بدلاً من message queue ومعرف الطابور queue ID بدلاً من message queue ID.


لإنشاء طابور جديد أو بفتح طابور موجود نستخدم الطريقة new ، وعند إضافة رسالة جديدة فسوف تضاف الى نهاية الطابور بواسطة الطريقة snd ، وكل رسالة لديها حقل النوع والطول و بايتات البيانات الفعلية ( المقابلة للطول ) ، وجميعهم يحددون الى snd عندما الرسائلة تضاف الى الطابور ، ونقوم بجلب الرسائل من الطابور بواسطة الطريقة rcv ، وليس من الضروري جلب الرسالة في الطلب first-in, first-out حيث يمكننا جلب الرسائل المستندة على حقل نوعهم .

إنشاء طابور :

بامكان إنشاء نوعين من الطوابير فأما أن يكون private او public وهنا مثال لإنشاء طابور private :

use IPC::SysV qw(IPC_PRIVATE IPC_CREAT S_IRWXU);
use IPC::Msg;

my $queue = new IPC::Msg IPC_PRIVATE, S_IRWXU | IPC_CREAT

لدى الباني new وسيطين فاول وسيط معرف الطابور queue ID و الطابور من النوع الخاص IPC_PRIVATE وثاني وسيط هي صلاحيات الطابور المجتمعة من خلال IPC_CREAT لإنشاء طابور ، كذلك الطوابير لديها الصلاحيات كما في الملفات بعمل mask للمستخدم والمجموعة والصلاحيات الاخرى لكي يمنح التفاعل اكثر مع المستخدم لإنشاء الطابور حيث يمكن للمستخدم ان يكتب الية ولكن البرامج الاخرى للمستخدمين الاخرين user IDs يمكنهم القراءة فقط ، وفي المثال السابق يعطى لنوع الطابور الخاص جميع الصلاحية للمستخدم المالك بواسطة S_IRWXU التي تعلن في وحـدة Fcntl في الغالب لكن وحـدة IPC::SysV تقوم بإستيرادة لذا ليس من الضروري تضمين Fcntl في السكربت للإستخدام الصلاحية 0700 .

فأذا كان الطابور private فأن العملية التي إنشاءت الطابور وأي طفل مفرع من تلك العملية يمكن أن يصل الى الطابور ، واذا كانت الطابور public فأن اي عملية تعرف معرف الطابور queue ID يمكن أن تصل الية ، فالطابور مفيد ، لذلك يحتاج ان يكون لدية معرف او مفتاح Key لكي نستطيع من خلاله التعامل معة ، ويوضع هذا المفتاح بعدد صحيح فمثلاً لانشاء طابور public يمكن الكتابة إلية عن طريق عمليات المستخدم user ID المالك المشغلة وتمكين القراءة فقط لأخرين :

# create a queue for writing
my $queue = new IPC::Msg 10023, 0722 | IPC_CREAT;

يمكن الان لعمليات المستخدمين الاخرين من الوصول الى هذا الطابور مع

my $queue = new IPC::Msg 10023, 0200;

تعيد القيمة true الى المتغير $queue ويصبح كائن طابور أوغير معرف undef في حالة ان لم يكن موجود ، وبإفتراض نجاح في إعادة الكائن الصحيح نستطيع الان أن نقراء ونكتب من الطابور لتثبت establish الإتصالات بين العمليات .

نلاحظ عدم تحديد المفتاح SysV ID key عند إنشاء طابور private وعند الرغبة في إرسال مفتاح الطابور إلى عملية اخرى علينا بتثبت الإتصال أولاً ثم نستطيع إستخراج ID من خلال الطريقة id التالية:

my $queue = new IPC::Msg IPC_PRIVATE, 0722 | IPC_CREAT;
my $id = $queue->id;

ولإرسال رسالة نستعمل الطريقة snd

الصيغة :

$queue->snd($type, $message, $flags);

الرسائل $message هي البيانات التي نرغب بإرسالها ، ونوع الرسالة $type هو عدد صحيح integer مستخدم ايضاً من قبل الطريقة rcv لتحديد الرسائل المختلفة المستندة على نوعهم ،ثم وسيط الأعلام $flags خياري ويمكن ان يوضع بالـIPC_NOWAIT إذ لم يتم إرسال الرسالة مباشرتاً ومن المحتمل تعيد الطريقة ثوابت كود الخطأ error المستوردة بواسطة الموديل ( في هذة الحالة $! سيوضع الى EAGAIN).

والطريقة rcv لإستلام الرسائل :

my $message;
$queue->rcv(\$message, $length, $type, $flags);

يجب ان يكون أول وسيط متغير scalar الى الرسائلة التي تقرأ ، ويعرف ثاني وسيط $length الطول الأقصي للرسالة التي تستلم وأذا كانت الرسالة اكبر من هذا الطول فأن الطريق rcv سوف تعيد undef والمتغير الخاص $! يوضع الى E2BIG ،والنوع $type يمنح لنا السيطرة على أي رسالة نستلمها وهو عدد صحيح integerكما في الجدول التالي( يدعنا وسيط نوع الرسالة من تحديد الرسالة التي نرغب إستلامها ) :

القيمة المعنى
type==0 إقرأ الرسالة الأولى على الطابور بغض النظر عن النوع .
type > 0 إقرأ الرسالة الأولى على الطابور من النوع المحدد ، فمثلاً اذا كان النوع 2 فسوف يقرأ الرسائل فقط من النوع 2 ، واذا لاشيء متؤفر أي لايوجد على الطابور رسائل من النوع 2 يسبب هذا للعملية بالتوقف block حتي يصبح واحد منه متؤفر .على اي حال شاهد IPC_NOWAIT و MSG_EXCEPT لاحقاً .
type < 0 إقرأ أبعد رسالة امامية في الطابور التي نوع الرسالة أقل أو يساوي القيمة المطلقة لنوع المحدد ، فمثلاً اذا النوع -2 فأن الرسائلة الاولي مع النوع 0 ستكون معادة ، واذا ليست هناك رسائل من النوع 0 في الحالي فأن الرسالة الاولى من النوع 1 هي من تعاد واذا ايضاً ليس هناك رسائل من النوع 1 في الحالي فأن الرسالة الاولي من النوع 2 تعاد .


وقد يأتي علم أو اكثر من التالي :

العلم الحدث
MSG_EXCEPT يعكس معني النوع حتي لقيمة الصفر أو القيم الموجبة تقوم بإسترجاع الرسالة الأولى ليست من النوع المحدد ، فمثلاً النوع 1 تسبب الطريقةrcv لإعادة الرسالة الأولى ليست من النوع 1
MSG_NOERROR يسمح للرسائل ذات الحجم الزائد outsize لكي تستلم ثم قطعهم الى الطول المحدد بدلاًُ من إعادة E2BIG كخطأ .
IPC_NOWAIT لاتنتظر للرسالة من النوع المطلوب اذ لاتوجد هناك رسالة متؤفرة ، ولكنها تعاد مع $! ويوضع الى EAGAIN.


بوضع كلتا الدالتين معاً يسمح لنا بوضع اعدادءات ماتسمى multilevel communications queue بمختلف أنواع الرسائل ومختلف الأغراض ، وكلياً النوع عائد على استخدامنا ويمكن ان يستعمل لإرسال رسائل مختلفة الى عمليات الطفل المختلفة أو الخيوط threads المضمنة في التطبيق ، وإيضاً يمكن أن يستخدم للأولوية priority أو كصانع قناة out-of-band ستناقش لاحقاً في TCP Urgent Data .

#!/usr/bin/perl
use IPC::SysV qw(IPC_CREAT);

use IPC::Msg;

# create a queue for writing
my $queue = new IPC::Msg 10023, 0722 | IPC_CREAT;

print ("Enter some text: ");
my $message=<>;
chomp($message);
$queue->snd(1, $message,IPC_NOWAIT);

#!/usr/bin/perl
use IPC::SysV qw(IPC_CREAT);
use IPC::Msg;

# create a queue for writing
my $queue = new IPC::Msg 10023, 0722 ;

my $message;

$bac=$queue->rcv($message, 5, 0, IPC_NOWAIT); 
print "$bac you wrote : ",$message,"\n";
printf("You wrote: %s", $message);

صلاحيات الطابور يمكن ان تغير بواسطة الطريقة set التي تعبى بقائمة من زوج key-value :

$queue->set (
uid => $user_id,        # i.e., like 'chmod' for files
gid => $group_id,       # i.e., like 'chgrp' for files
mode => $permissions,   # an octal value or S_ flags
qbytes => $queue_size   # the queue's capacity
)

ويمكننا ايضاً إسترجاع الكائن IPC::Msg::stat الذي يمكن فية أن ينشأ ويعدل وترقية خصائص الطابور عن طريق الطريقة stat :

my $stat = $queue->stat;
$stat->mode(0722);
$queue->set($stat);

فالكائن المعاد من خلال الطريقة stat هو بالفعل كائن مستند على الطبقة Class::Struct التي توفر طرق get/set التالية ( وجميعها لاتعالج مباشرتاً في تركيب stat ) :

الطريقة الغرض
uid تحديد معرف المستخدم الفعال effective UID لطابور الذي يشتغيل علية
gid تحديد معرف المجموعة الفعال effective GID لطابور الذي تشغل علية
cuid* طابور معرف المستخدم UID بدأ معة
cgid* طابور معرف المجموعة GID بدأ معة
mode عديد من التصاريح على الطابور
qnum* عدد الرسائل الحالية في الطابور
qbytes حجم طابور الرسائل
lspid* PID للعملية الأخيرة التي إرسلت اخيراً الى الطابور
lrpid* PID للعملية الأخيرة التي إستلمت اخيراً من الطابور
stime* وقت الإرسال الأخير في الطابور
rtime* وقت الإستلام الأخير في الطابور
ctime* وقت التغير الأخير في تركيبة stat


لاحظ التغير الى الكائن المعاد بواسطة الطريقة stat لاتعمل الترقية مباشرتاً لخواص الطابور فسنحتاج الى هذا الحدث أما عن طريق امرار كائن stat ليكون مرقى أو امرار أزواج key-value مستقلة (تستخدم المفاتيح المندرجة في القائمة السابقة كما تعمل طرق stat ) لطريقة set

لتحرير الطابور من الذاكرة في الاخير نستدعى remove بإفتراض لدينا الصلاحية لعمل ذلك:

$queue->remove;

يعيد undef اذا لم يتم تحرير الطابور مع السبب في المتغير الخاص $! غالباً يوضع EPERM ، على اي حال من الضرورة أزلة الطابور لكثير المبرمجين وعمل التتضيفات اللأزمة للذاكرة عند خروج البرنامج .



Semaphores

السيمافورات IPC semaphores هي مجموعة من الأعلام العددية في الذاكرة (تعرف ايضاً بالـمجموعات السيمافور semaphore sets أو مصفوفة السيمافور) ويمكن أن نقرأ أو نكتب إلية من مختلف العمليات للإشارة إلى الحالات المختلفة ، ويمكن ان يكون private أو public ويمتلك معرف id خاص الذي يمكن من خلالة الوصول مع تصريحات mask لكي يسيطرون على العمليات وترتيب الوصول.

تستخدم السيمافورات للغرضين:

تضمن وحـدة IPC::Semaphore في اسكربت البيرل لوصول الى السيمافورات في الذاكرة:

use IPC::SysV qw(IPC_CREAT IPC_PRIVATE S_IRWXU);
use IPC::Semaphore;

my $size = 4;
my $sem = new IPC::Semaphore(IPC_PRIVATE, $size, IPC_CREAT | S_IRWXU);

في المثال ينشأ مجموعة سيمافور خاصة IPC_PRIVATE فية أربع سيمافورات ، والصلاحية للمالك S_IRWXU (قراءة/كتابة/تنفيذ) .

ولإنشاء مجموعة سيمافور عامة public من خلال تعديل قيمة المفتاح الى عددي :

my $sem = new IPC::Semaphore(10023, 4, 0722 | IPC_CREAT);

الان يمكن وصول العمليات الاخرى الى السيمافور بواسطة التالي

my $sem = new IPC::Semaphore(10023, 4, 0200);   # or S_IRDONLY

كما هو الحال لإستراجاع مفتاح مجموعة السيمافور مع الطريقة id

$id = $sem->id;

نستطيع اذا كانت لدينا الصلاحية اللازمة إمكانية التلاعب من خلال الطرق المختلفة التي توجد في ملفات المساعدة وهنا قائمة منها في الجدول :

الاسم الحدث
getall لإعادة جميع القيم كالقائمة، فمثلاً

my @semvals = $sem->getall;

getval لإعادة قيمة السيمافور المحدد، فمثلاً للحصول على قيمة السيمافور الرابع

# first semaphore is 0, so 4th is 3 my $sem4 = $sem->getval(3);

setall وضع القيمة المحددة لجميع قيم مجموعة السيمافور، فعلي سبيل المثال لوضع قيمة 0 للجميع

$sem->setall( (0) x 4 );

setval وضع القيمة المحددة للسيمافور المحدد ،فعلي سبيل المثال لوضع القيمة 1 الى قيمة السيمافور الرابع

# set value of 4th semaphore to 1 $sem->setval(3, 1);

set ضع الـuser ID او group ID او الصلاحيات الى السيمافور ، علي سبيل المثال

$sem->set( uid => $user_id, gid => $group_id, mode => $permissions, );



بدلاً من طرق وضع وحصول وتلاعب get, manipulate, and set السابقة يمكننا ذلك بإستخدام كائن stat المعاد بواسطة الطريقة stat في نفس إسلوب كائنات IPC::Msg :

الاسم الحدث
stat بتوليد كائن من IPC::Semaphore::stat يمكن التلاعب، ومن ثم يستخدم مع set ، فعلي سبيل المثال :

$semstat = $sem->stat; $semstat->mode(0722); $sem->set($semstat);

getpid لإعادة الـprocess ID للعملية الاخيرة التي تعاملت مع مجموعة السيمافور لإداء الوظيفة بواسطةsemop .
getncnt لإعادة عدد العمليات التي نفذت الـ semop والمتنظرة (blocked) لقيمة السيمافور المحدد لكي يزتاد في القيمة .

$ncnt = $sem->getncnt;

getzcnt لإعادة عدد العمليات التي نفذت الـ semop والمتنظرة (blocked) لقيمة السيمافور المحدد لكي يصبح صفراً.


التعامل مع السيمافور:

يربط السيمافور مع الطريقة op التي تؤدي العديد من الوظائف على مجموعة السيمافور وتستعمل لعمل أنتظار او ايقاض العملية بواسطة العمليات الاخرى .

تشمل كل وظيفة operation ثلاثة قيم $sem->op: رقم السيمافور للعمل علية والوظيفة التي يؤديها وقيمة العلم .

الوظيفة التي يؤديها هي القيمة لزيادت أو لإنقاص السيمافور وهنا القواعد التالية :

يمكننا إختيار لتعامل مع العديد من السيمافورات كما نحب ، فجميع التعاملات operations يجب ان تكون قادرة على الإكتمال وقبل التعامل مع الـoperation اخرى لنجاح ،فعلي سبيل المثال

$sem->op(
0, -1, 0,   # decrement semaphore 1
1, -1, 0,   # decrement semaphore 2
3,  0, 0,   # semaphore 3 must be zero
);

فالقواعد للـblocking على السيمافورات تسمح لنا لإنشاء التطبيقات التي يمكن أن تتعاون مع بعضهم البعض ، فمثلاً التطبيق يسيطر على التنفيذ الاخر بوضع اعداءات لقيم السيمافور . فالتطبيقات يمكن أن ينسق ايضاً للوصول الى المصادر المشاركة ، وهذا موضوع كبير فعلاً لهذا سنعطي فقط مثال إيضاحي بسيط لكيفية عمل السيمافور المنسق للوصول إلى المصدر المشارك المشترك :

  1. التطبيق 1 ينشأ مجموعة الـsemaphore بإسيمافور واحد ، والقيمة 1 ،وينشأ المصدر المشترك ،فعلى سبيل المثال الملف أو الـIPC shared memory segment ، على أي حال هو يقرر لعمل الكثير من القيم الإبتدائية وهكذا هو لايمكنة الوصول الى المصدر مباشرتاً.

  2. التطبيق 2 يبدأ ، ويناقص الـsemaphore الى 0 ، ويصل المصدر المشترك

  3. التطبيق 1 الآن سوف يحاول الى إنقاص الـsemaphore ويصل المصدر .الـsemaphore هو 0 ،لذا فهو لايمكنة الوصول الية ولذلك سوف blocks.

  4. التطبيق 2 ينتهي بالمصدر المشترك ويزيد الـ semaphore والـoperation تلك تنجح دائماً

  5. التطبيق 1 يمكن الآن أن ينقص الـsemaphore لكؤنة الان اصبح 1 ، وهكذا فالـoperation تنجح ولم تعد متكتلة blocks

  6. التطبيق 2 يحاول للوصول الـمصدر المرة الثانية ، فأولاً هو يحاول لإنقاص الـsemaphore لكنة غير ممكن او غير قادر على ذلك ويؤقف blocks

  7. التطبيق 1 ينتهي ويزيد الـsemaphore .

  8. التطبيق 2 ينقص الـsemaphore ويصل الـمصدر

وهكذا..........

بالرغم من أنة يبدو معقداً ، فهو في الواقع بسيط جداً ، في الكود فأن كل تطبيق ببساطة يصل الـsemaphore ، وينشأدة اذا لم يكن الحالي ، ثم يضيف سطرين حول كل الوصولات الى المصدر للكي يكون محمي:

sub access_resource {
# decrement semaphore, blocking if it is already zero
$sem->op(0, -1, 0);
... access resource ...
# increment semaphore, allowing access by other processes
$sem->op(0, 1, 0);
}

وأن كان لدينا أكثر من مصدر للسيطرة علية ، فنحن فحسب نشأ مجموعة الـsemaphore بإكثر من سيمافور .

فشئ الإساسي لهذة النظرة لتلك التطبيقات بانها توافق على التعاون من خلال السيمافور ،وكل واحداً لدية مفتاح للاسيمافور (لأنة سوف يعطى في ملف الإعداءدات مثلاً ) ويصبح الـbasis الوحيد للإتصال بينهم ، بالرغم من أن المصدر مسيطر علية وليس لة أتصال مباشر الى السمافور ، فكل تطبيق دائماً يشرف الية قبل وصولة . فالسيمافور يصبح البؤاب أو gatekeeper ويسمح لوصول تطبيق واحد فقط في كل مرة ، وإذا لم نرغب الى الـblock بينما ينتظر للاسيمافور يمكن ان نحدد الـIPC_NOWAIT للقيمة العلم ، ويمكننا فعل هذا على الـ“per semaphore basis” أيضاً ، فأن رغببنا بهذا مع ذلك يمكن ان يكون مشوشين فعلى سبيل المثال

sub access_resource {
return undef unless $sem->op(0, -1, IPC_NOWAIT);
... access resource ...
$sem->op(0, 1, 0);
}

يمكننا أن نضغ العلم SEM_UNDO ( إذا نحن إستوردة من IPC::SysV أولاً ) ليسبب لوظيفة السيمافور semaphore operation لعمل undone بشكل آلي اذا خرجت exits العملية اما deliberately او بسبب خطأ error ، وهذا مفيد في التطبيقات الممنوعة التي تجهض abort بينما يقفل المصدر وثم بعد ذلك لايفك إرتباطة releasing ابداً مرة اخرى فعلي سبيل المثال :

$sem->op(0, -1, IPC_NOWAIT | SEM_UNDO);
die unless critical_subroutine();

كما هو الحال مع message queues يجب الاهتمام بأن نعطي لكي لايغادر الsegments الغير مستخدمة حول بعد أن تخرج العملية الأخيرة . سنعود إلى موضوع الـsemaphores لاحقاً في الthreads التي لديها آلية الـsemaphore الخاصة بها ،استشق كثيراً في الفصل السابق

semaphore operation اختصارها semop

هنا برنامج للسيطرة على الوصول للـbuffer المشارك في الذاكرة للعميات مشتقة خاصة ثم للوصول الامن سوف نشأ الـsemaphore لكل قطعة ، فمثلاً نقوم بإنشاء زوج من semaphores لكل bit من الذاكرة المشاركة حيث واحد يكون للقراءة والاخر للكتابة وفي الحقيقة هذا ماتعملة الموديل IPC::Shareable وتستطيع اداء الوضيفة التي يؤديها على جميع الsemaphores بشكل واحد .

فعندما ترغب بالحصول او بوضع القيمة الجديدة الى الذاكرة المشاركة فيجب عليك ان تذهب الى أن تمر الى الـsemaphore الاول ، وهذا مايصبح مضجراً ولذلك نغلف الوصول في كائن الطبقة ، حيث IPC::Shareable تذهب الى الخطوة واحد وتغلف كائن الطبقة مع وصلة tie .

فالبرنامج التالي يشغل حتي ضغط مفتاح الاعتراض Control-C :

#!/usr/bin/perl -w
use v5.6.0;   # or better
use strict;
use sigtrap qw(die INT TERM HUP QUIT);

use lib "/home/back/";
use ShMem;


my $PROGENY = shift(@ARGV) || 3;
eval { main() };   # see DESTROY below for why
die if $@ && $@ !~ /^Caught a SIG/;
print "\nDone.\n";
exit;

sub main {
    my $mem = ShMem->alloc("Original Creation at " . localtime);
    my(@kids, $child);
    $SIG{CHLD} = 'IGNORE';
    for (my $unborn = $PROGENY; $unborn > 0; $unborn--) {
        if ($child = fork) {
            print "$$ begat $child\n";
            next;
        }
        die "cannot fork: $!" unless defined $child;
        eval {
            while (1) {

                $mem->lock();
                $mem->poke("$$ " . localtime) 
                    unless $mem->peek =~ /^$$\b/o;
                $mem->unlock();
            }
        };

        die if $@ && $@ !~ /^Caught a SIG/;
        exit;  # child death

    }
    while (1) {
        print "Buffer is ", $mem->get, "\n";
        sleep 1;
    }
}
back@ubuntu:~$ perl ShMem.pl 5663 begat 5664 5663 begat 5665 5663 begat 5666 Buffer is 5664 Sat Nov 15 15:43:05 2008 Buffer is 5664 Sat Nov 15 15:43:06 2008 Buffer is 5665 Sat Nov 15 15:43:07 2008 Buffer is 5666 Sat Nov 15 15:43:08 2008 Buffer is 5665 Sat Nov 15 15:43:09 2008 Buffer is 5664 Sat Nov 15 15:43:10 2008 Buffer is 5664 Sat Nov 15 15:43:11 2008 Buffer is 5666 Sat Nov 15 15:43:12 2008 Buffer is 5666 Sat Nov 15 15:43:13 2008 Buffer is 5665 Sat Nov 15 15:43:14 2008 Buffer is 5664 Sat Nov 15 15:43:15 2008 Buffer is 5666 Sat Nov 15 15:43:16 2008 Buffer is 5666 Sat Nov 15 15:43:17 2008 Buffer is 5665 Sat Nov 15 15:43:18 2008 Buffer is 5665 Sat Nov 15 15:43:19 2008 Buffer is 5665 Sat Nov 15 15:43:20 2008 Buffer is 5664 Sat Nov 15 15:43:21 2008 Done. back@ubuntu:~$

package ShMem;
use IPC::SysV qw(IPC_PRIVATE IPC_RMID IPC_CREAT S_IRWXU);
use IPC::Semaphore;
sub MAXBUF() { 2000 }

sub alloc {    # constructor method
    my $class = shift;
    my $value = @_ ? shift : '';

    my $key = shmget(IPC_PRIVATE, MAXBUF, S_IRWXU) or die "shmget: $!";
    my $sem = IPC::Semaphore->new(IPC_PRIVATE, 1, S_IRWXU | IPC_CREAT)
                         or die "IPC::Semaphore->new: $!";
    $sem->setval(0,1)    or die "sem setval: $!";

    my $self = bless {
        OWNER   => $$,
        SHMKEY  => $key,
        SEMA    => $sem,
    } => $class;

    $self->put($value);
    return $self;
}



sub get {
    my $self = shift;
    $self->lock;
    my $value = $self->peek(@_);
    $self->unlock;
    return $value;
}
sub peek {
    my $self = shift;
    shmread($self->{SHMKEY}, my $buff='', 0, MAXBUF) or die "shmread: $!";
    substr($buff, index($buff, "\0")) = '';
    return $buff;
}
sub put {
    my $self = shift;
    $self->lock;
    $self->poke(@_);
    $self->unlock;
}
sub poke {
    my($self,$msg) = @_;
    shmwrite($self->{SHMKEY}, $msg, 0, MAXBUF) or die "shmwrite: $!";
}
sub lock {
    my $self = shift;
    $self->{SEMA}->op(0,-1,0) or die "semop: $!";
}
sub unlock {
    my $self = shift;
    $self->{SEMA}->op(0,1,0) or die "semop: $!";
}

sub DESTROY {
    my $self = shift;
    return unless $self->{OWNER} == $$;  # avoid dup dealloc
    shmctl($self->{SHMKEY}, IPC_RMID, 0)    or warn "shmctl RMID: $!";
    $self->{SEMA}->remove()                 or warn "sema->remove: $!";
}

1;